/**
 * Copyright 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactDOMInput
 */

'use strict'

var _assign = require('object-assign')

var DOMPropertyOperations = require('./DOMPropertyOperations')
var LinkedValueUtils = require('./LinkedValueUtils')
var ReactDOMComponentTree = require('./ReactDOMComponentTree')
var ReactUpdates = require('./ReactUpdates')

var invariant = require('fbjs/lib/invariant')
var warning = require('fbjs/lib/warning')

var didWarnValueLink = false
var didWarnCheckedLink = false
var didWarnValueNull = false
var didWarnValueDefaultValue = false
var didWarnCheckedDefaultChecked = false
var didWarnControlledToUncontrolled = false
var didWarnUncontrolledToControlled = false

function forceUpdateIfMounted() {
  if (this._rootNodeID) {
    // DOM component is still mounted; update
    ReactDOMInput.updateWrapper(this)
  }
}

function warnIfValueIsNull(props) {
  if (props != null && props.value === null && !didWarnValueNull) {
    process.env.NODE_ENV !== 'production'
      ? warning(
          false,
          '`value` prop on `input` should not be null. ' +
            'Consider using the empty string to clear the component or `undefined` ' +
            'for uncontrolled components.'
        )
      : void 0

    didWarnValueNull = true
  }
}

/**
 * Implements an <input> native component that allows setting these optional
 * props: `checked`, `value`, `defaultChecked`, and `defaultValue`.
 *
 * If `checked` or `value` are not supplied (or null/undefined), user actions
 * that affect the checked state or value will trigger updates to the element.
 *
 * If they are supplied (and not null/undefined), the rendered element will not
 * trigger updates to the element. Instead, the props must change in order for
 * the rendered element to be updated.
 *
 * The rendered element will be initialized as unchecked (or `defaultChecked`)
 * with an empty value (or `defaultValue`).
 *
 * @see http://www.w3.org/TR/2012/WD-html5-20121025/the-input-element.html
 */
var ReactDOMInput = {
  getNativeProps: function (inst, props) {
    var value = LinkedValueUtils.getValue(props)
    var checked = LinkedValueUtils.getChecked(props)

    var nativeProps = _assign(
      {
        // Make sure we set .type before any other properties (setting .value
        // before .type means .value is lost in IE11 and below)
        type: undefined
      },
      props,
      {
        defaultChecked: undefined,
        defaultValue: undefined,
        value: value != null ? value : inst._wrapperState.initialValue,
        checked: checked != null ? checked : inst._wrapperState.initialChecked,
        onChange: inst._wrapperState.onChange
      }
    )

    return nativeProps
  },

  mountWrapper: function (inst, props) {
    if (process.env.NODE_ENV !== 'production') {
      LinkedValueUtils.checkPropTypes(
        'input',
        props,
        inst._currentElement._owner
      )

      if (props.valueLink !== undefined && !didWarnValueLink) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              '`valueLink` prop on `input` is deprecated; set `value` and `onChange` instead.'
            )
          : void 0
        didWarnValueLink = true
      }
      if (props.checkedLink !== undefined && !didWarnCheckedLink) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              '`checkedLink` prop on `input` is deprecated; set `value` and `onChange` instead.'
            )
          : void 0
        didWarnCheckedLink = true
      }
      if (
        props.checked !== undefined &&
        props.defaultChecked !== undefined &&
        !didWarnCheckedDefaultChecked
      ) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              'Input elements must be either controlled or uncontrolled ' +
                '(specify either the checked prop, or the defaultChecked prop, but not ' +
                'both). Decide between using a controlled or uncontrolled input ' +
                'element and remove one of these props. More info: ' +
                'https://fb.me/react-controlled-components'
            )
          : void 0
        didWarnCheckedDefaultChecked = true
      }
      if (
        props.value !== undefined &&
        props.defaultValue !== undefined &&
        !didWarnValueDefaultValue
      ) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              'Input elements must be either controlled or uncontrolled ' +
                '(specify either the value prop, or the defaultValue prop, but not ' +
                'both). Decide between using a controlled or uncontrolled input ' +
                'element and remove one of these props. More info: ' +
                'https://fb.me/react-controlled-components'
            )
          : void 0
        didWarnValueDefaultValue = true
      }
      warnIfValueIsNull(props)
    }

    var defaultValue = props.defaultValue
    inst._wrapperState = {
      initialChecked: props.defaultChecked || false,
      initialValue: defaultValue != null ? defaultValue : null,
      listeners: null,
      onChange: _handleChange.bind(inst)
    }

    if (process.env.NODE_ENV !== 'production') {
      inst._wrapperState.controlled =
        props.checked !== undefined || props.value !== undefined
    }
  },

  updateWrapper: function (inst) {
    var props = inst._currentElement.props

    if (process.env.NODE_ENV !== 'production') {
      warnIfValueIsNull(props)

      var initialValue =
        inst._wrapperState.initialChecked || inst._wrapperState.initialValue
      var defaultValue = props.defaultChecked || props.defaultValue
      var controlled = props.checked !== undefined || props.value !== undefined
      var owner = inst._currentElement._owner

      if (
        (initialValue || !inst._wrapperState.controlled) &&
        controlled &&
        !didWarnUncontrolledToControlled
      ) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              '%s is changing a uncontrolled input of type %s to be controlled. ' +
                'Input elements should not switch from uncontrolled to controlled (or vice versa). ' +
                'Decide between using a controlled or uncontrolled input ' +
                'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
              (owner && owner.getName()) || 'A component',
              props.type
            )
          : void 0
        didWarnUncontrolledToControlled = true
      }
      if (
        inst._wrapperState.controlled &&
        (defaultValue || !controlled) &&
        !didWarnControlledToUncontrolled
      ) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              false,
              '%s is changing a controlled input of type %s to be uncontrolled. ' +
                'Input elements should not switch from controlled to uncontrolled (or vice versa). ' +
                'Decide between using a controlled or uncontrolled input ' +
                'element for the lifetime of the component. More info: https://fb.me/react-controlled-components',
              (owner && owner.getName()) || 'A component',
              props.type
            )
          : void 0
        didWarnControlledToUncontrolled = true
      }
    }

    // TODO: Shouldn't this be getChecked(props)?
    var checked = props.checked
    if (checked != null) {
      DOMPropertyOperations.setValueForProperty(
        ReactDOMComponentTree.getNodeFromInstance(inst),
        'checked',
        checked || false
      )
    }

    var value = LinkedValueUtils.getValue(props)
    if (value != null) {
      // Cast `value` to a string to ensure the value is set correctly. While
      // browsers typically do this as necessary, jsdom doesn't.
      DOMPropertyOperations.setValueForProperty(
        ReactDOMComponentTree.getNodeFromInstance(inst),
        'value',
        '' + value
      )
    }
  }
}

function _handleChange(event) {
  var props = this._currentElement.props

  var returnValue = LinkedValueUtils.executeOnChange(props, event)

  // Here we use asap to wait until all updates have propagated, which
  // is important when using controlled components within layers:
  // https://github.com/facebook/react/issues/1698
  ReactUpdates.asap(forceUpdateIfMounted, this)

  var name = props.name
  if (props.type === 'radio' && name != null) {
    var rootNode = ReactDOMComponentTree.getNodeFromInstance(this)
    var queryRoot = rootNode

    while (queryRoot.parentNode) {
      queryRoot = queryRoot.parentNode
    }

    // If `rootNode.form` was non-null, then we could try `form.elements`,
    // but that sometimes behaves strangely in IE8. We could also try using
    // `form.getElementsByName`, but that will only return direct children
    // and won't include inputs that use the HTML5 `form=` attribute. Since
    // the input might not even be in a form, let's just use the global
    // `querySelectorAll` to ensure we don't miss anything.
    var group = queryRoot.querySelectorAll(
      'input[name=' + JSON.stringify('' + name) + '][type="radio"]'
    )

    for (var i = 0; i < group.length; i++) {
      var otherNode = group[i]
      if (otherNode === rootNode || otherNode.form !== rootNode.form) {
        continue
      }
      // This will throw if radio buttons rendered by different copies of React
      // and the same name are rendered into the same form (same as #1939).
      // That's probably okay; we don't support it just as we don't support
      // mixing React radio buttons with non-React ones.
      var otherInstance = ReactDOMComponentTree.getInstanceFromNode(otherNode)
      !otherInstance
        ? process.env.NODE_ENV !== 'production'
          ? invariant(
              false,
              'ReactDOMInput: Mixing React and non-React radio inputs with the ' +
                'same `name` is not supported.'
            )
          : invariant(false)
        : void 0
      // If this is a controlled radio button group, forcing the input that
      // was previously checked to update will cause it to be come re-checked
      // as appropriate.
      ReactUpdates.asap(forceUpdateIfMounted, otherInstance)
    }
  }

  return returnValue
}

module.exports = ReactDOMInput
